/* shell.c, Ait, BSD 3-Clause, Kevin Bloom, 2023-2025 */

#include <stdio.h>
#include <sys/wait.h>
#include "header.h"
#include "termbox.h"
#include "util.h"

#define MAX_COMMANDS 10
#define MAX_ARG_LEN 100
#define BUFFER_SIZE 256

char opcmdtext[STRBUF_M];
char ipcmdtext[STRBUF_M];
int oline = 0, ib = FALSE;

/* TODO: make this generic
   M-e will display "Shell Command: " in the msgline. You then input the command
   you want.
   Eventually maybe make it so that there are different types of commands:
   - input, inputs something at the point
   - open, runs a command and ait will open the output (this currently works)
   - region/replace, use the region as the input to the shell cmd and then
     replace the region with the output
   - new buffer, runs the command and the output is placed in a new buffer

   I probably would want some keybinds to certain commands, however.
   Also, I'd like to make it so that if you have a region selected, it can be
   executed much like how acme does it.

   You can input the current buffer's filename (including its path),
   line number, and column number by placing a single @, #, or $,
   respectively. If the command begins with !, it will _not_ put
   output anything into the temp file (which is required for ait to
   read the output). This will allow you to write generic interactive
   scripts using the shell read command.

   io = Insert = 0, Open = 1
*/
void get_popen_data() {
  char *command = NULL;
  buffer_t *bp;
  char *insertp = "Shell Command", *openp = "Open Via";
  char prompt[STRBUF_M + 12 + strlen(insertp)];
  int cpos = 0, done = FALSE, ots = FALSE;
  int c = 0, k = 0, hasregion = FALSE, escaped_size = 0;
  int start_col = strlen(prompt), didtry, fd;
  int line = 0, onscrap = 0, ishellring = -1;
  int newlines = 0;
  struct tb_event ev;
  char cmdtext[STRBUF_L], *cbuf; /* cbuf is the colon buffer for line number */
  memset(cmdtext, 0, STRBUF_L);
  point_t point;
  char_t *oscrap = NULL, *escaped_region = NULL, *kr;
  const char* path = getenv("PATH");
  FILE *file, *fp = NULL;
  struct stat sb;
  char sys_command[CHUNK];
  static char temp_file[] = TEMPFILE;

  oline = curbp->b_line;

  if(is_file_modified(curbp->b_fname) && !file_was_modified_prompt()) {
    return;
  }

  if(io == 1) {
    sprintf(prompt, "%s", openp);
    if(opcmdtext[0] != '\0') {
      strcat(prompt, " (default ");
      strcat(prompt, opcmdtext);
      strcat(prompt, ")");
    }
  } else {
    sprintf(prompt, "%s", insertp);
    if(ipcmdtext[0] != '\0') {
      strcat(prompt, " (default ");
      strcat(prompt, ipcmdtext);
      strcat(prompt, ")");
    }
  }

  strcat(prompt, ": ");

  start_col = strlen(prompt);

  display_prompt_and_response(prompt, cmdtext);
  cpos = strlen(cmdtext);

  for (;;) {
    didtry = (k == 0x09);  /* Was last command tab-completion? */
    tb_present();
    if(execute_kbd_macro) {
      use_kbd_macro(&ev);
    } else if(tb_poll_event(&ev) != TB_OK) return;

    if(msgline_editor(ev, prompt, cmdtext, STRBUF_L, &cpos)) {
      continue;
    }

    if(!ev.mod && ev.key != TB_KEY_ARROW_UP && ev.key != TB_KEY_ARROW_DOWN)
      k = ev.ch;
    else
      k = ev.key;

    if(record_input) {
      record_buffer[record_buffer_index] = ev;
      record_buffer_index++;
    }

    /* ignore control keys other than return, C-g, backspace, CR,  C-s, C-R, ESC, tab */
      if (k < 32 &&
          k != TB_KEY_CTRL_G &&
          k != TB_KEY_CTRL_N &&
          k != TB_KEY_CTRL_P &&
          k != TB_KEY_ARROW_UP &&
          k != TB_KEY_ARROW_DOWN &&
          k != TB_KEY_BACKSPACE &&
          k != TB_KEY_BACKSPACE2 &&
          k != TB_KEY_ENTER &&
          k != TB_KEY_ESC &&
          k != TB_KEY_TAB)
      continue;

    switch(k) {
    case TB_KEY_ENTER: /* return */
      done = TRUE;
      break;

    case TB_KEY_ARROW_DOWN:
    case TB_KEY_CTRL_N: /* ctrl-n */
      if(ishellring > 0)
        ishellring--;
      else
        break;
      cpos = 0;
      for (int i = 0; i < shell_ring[ishellring].len; i++) {
        cmdtext[cpos++] = shell_ring[ishellring].data[i];
        cmdtext[cpos] = '\0';
      }
      cpos = shell_ring[ishellring].len;
      cmdtext[cpos] = '\0';
      tb_set_cursor(start_col, MSGLINE);
      clrtoeol(start_col, MSGLINE);
      addstr(cmdtext);
      break;

    case TB_KEY_ARROW_UP:
    case TB_KEY_CTRL_P: /* ctrl-p */
      if(ishellring < SHELLRING_SIZE -1 && shell_ring[ishellring+1].data != NULL)
        ishellring++;
      else
        break;
      cpos = 0;
      for (int i = 0; i < shell_ring[ishellring].len; i++) {
        cmdtext[cpos++] = shell_ring[ishellring].data[i];
        cmdtext[cpos] = '\0';
      }
      cpos = shell_ring[ishellring].len;
      cmdtext[cpos] = '\0';
      tb_set_cursor(start_col, MSGLINE);
      clrtoeol(start_col, MSGLINE);
      addstr(cmdtext);
      break;

    case TB_KEY_ESC: /* esc */
    case TB_KEY_CTRL_G: /* ctrl-g */
      if (fp != NULL) fclose(fp);
      tb_set_cursor(0, MSGLINE);
      clrtoeol(0, MSGLINE);
      return;

    case TB_KEY_BACKSPACE2: /* del, erase */
    case TB_KEY_BACKSPACE: /* backspace */
      if (cpos == 0)
        continue;
      cmdtext[--cpos] = '\0';
      tb_set_cursor(start_col + cpos, MSGLINE);
      display_prompt_and_response(prompt, cmdtext);
      break;

do_tab:
    case TB_KEY_TAB: {
      char curpath[PATH_MAX], pcmd[PATH_MAX], cu;
      int ii = 0;
      for(cu = cmdtext[cpos]; !isspace(cu) && cpos > 0; cpos--, cu = cmdtext[cpos])
        ;;
      for(int iii = 0; cmdtext[cpos+iii] != '\0'; iii++) {
        cu = cmdtext[cpos+iii];
        if(!isspace(cu)) {
          pcmd[ii] = cu;
          ii++;
        }
      }
      if(cpos > 0)
        cpos++;
      pcmd[ii] = '\0';
      if(!didtry) {
        if (fp != NULL) fclose(fp);
        strcpy(temp_file, TEMPFILE);
        if (-1 == (fd = mkstemp(temp_file)))
          fatal("%s: Failed to create temp file\n");
        strcpy(sys_command, "printf \"%s\\n\" ");
        for(int i = 0, ii = 0; path[i] != '\0'; i++, ii++) {
          if(path[i] == ':') {
            curpath[ii] = '\0';
            strcat(sys_command, curpath);
            strcat(sys_command, "/");
            strcat(sys_command, pcmd);
            strcat(sys_command, "* ");
            ii = 0;
            i++;
          } else
            curpath[ii] = path[i];
        }
        strcat(sys_command, "| awk '$0 !~ \"\\\\*\" { split($0, a, \"/\"); print a[length(a)] }' | sort -u >");
        strcat(sys_command, temp_file);
//         strcat(sys_command, " 2>&1");
        (void) ! system(sys_command); /* stop compiler unused result warning */
        fp = fdopen(fd, "r");
        unlink(temp_file);
      }

      while ((c = getc(fp)) != EOF && c != '\n') {
        if (cpos < PATH_MAX - 1 && c != '*') {
          cmdtext[cpos++] = c;
          cmdtext[cpos] = '\0';
        }
      }

      cmdtext[cpos] = '\0';
      for(int i = cpos+1; cmdtext[i] != '\0'; i++)
        cmdtext[i] = 0;
      if (c != '\n' || c == -1) rewind(fp);
      if(c == -1)  goto do_tab;
      didtry = 1;
      tb_set_cursor(start_col, MSGLINE);
      clrtoeol(start_col, MSGLINE);
      addstr(cmdtext);
      break;
    }

    default:
      if (cpos < STRBUF_M - 1) {
        for(int i = strlen(cmdtext); i > cpos; i--) {
          cmdtext[i] = cmdtext[i - 1];
        }
        cmdtext[cpos] = k;
        cmdtext[strlen(cmdtext)] = '\0';
        tb_set_cursor(start_col, MSGLINE);
        addstr(cmdtext);
        cpos++;
        tb_set_cursor(start_col + cpos, MSGLINE);
      }
      break;
    }
    if(done)
      break;
  }

  if(cmdtext[0] == '\0') {
    if(io == 1 && opcmdtext[0] != '\0')
      strncpy(cmdtext, opcmdtext, STRBUF_M);
    else if(io == 0 && ipcmdtext[0] != '\0')
      strncpy(cmdtext, ipcmdtext, STRBUF_M);
    else {
      clrtoeol(0, MSGLINE);
      return;
    }
  }

  if(io == 1)
    strncpy(opcmdtext, cmdtext, STRBUF_M);
  else if(io == 0)
    strncpy(ipcmdtext, cmdtext, STRBUF_M);

  int ncmd = 0, cncmd = 0;
  char n;
  for(int i = 0; cmdtext[i] != '\0'; i++, ncmd++) {
    n = cmdtext[i+1];
    if(cmdtext[i] == '@' && cmdtext[i-1] != '\\') {
      ncmd += strlen(curbp->b_fname);
    }
    if(cmdtext[i] == '#') {
      char *s;
      asprintf(&s, "%d", curbp->b_line);
      ncmd += strlen(s);
      free(s);
      s = NULL;
    }
    if(cmdtext[i] == '$' && !isalpha(n) &&
       cmdtext[i-1] != '\\') {
      char *s;
      asprintf(&s, "%d", curbp->b_col);
      ncmd += strlen(s);
      free(s);
      s = NULL;
    }
  }

  char cmd[ncmd];
  if(cmdtext[0] == '!') {
    ots = TRUE;
    cmdtext[0] = ' ';
    if(cmdtext[1] == '<') {
      ib = TRUE;
      cmdtext[1] = ' ';
    }
  } else if(cmdtext[0] == '<') {
    ib = TRUE;
    cmdtext[0] = ' ';
  }
  for(int i = 0; cmdtext[i] != '\0'; i++, cncmd++) {
    n = cmdtext[i+1];
    if(cmdtext[i] == '@' && cmdtext[i-1] != '\\') {
      cncmd += strlen(curbp->b_fname) - 1;
      strcat(cmd, curbp->b_fname);
    } else if(cmdtext[i] == '#') {
      char *s;
      asprintf(&s, "%d", curbp->b_line);
      cncmd += strlen(s) - 1;
      strcat(cmd, s);
      free(s);
      s = NULL;
    } else if(cmdtext[i] == '$' && isspace(n) &&
              cmdtext[i-1] != '\\') {
      char *s;
      asprintf(&s, "%d", curbp->b_col);
      cncmd += strlen(s) - 1;
      strcat(cmd, s);
      free(s);
      s = NULL;
    } else {
      cmd[cncmd] = cmdtext[i];
    }
    cmd[cncmd+1] = '\0';
  }

  if (curbp->b_mark != NOMARK && curbp->b_point != curbp->b_mark) {
    if(io == 0) {
      oscrap = (char_t*) malloc(scrap.len);
      onscrap = scrap.len;
      (void) memcpy(oscrap, scrap.data, scrap.len * sizeof (char_t));
      copy_cut(TRUE, TRUE, FALSE);
    }
    hasregion = TRUE;
  }

  tb_shutdown();

  if(hasregion && io == 0) {
    /* Find all dollar signs and increase the size by one for each sign. */
    for(int i = 0; scrap.data[i] != '\0'; i++) {
      if(scrap.data[i] == '$' || scrap.data[i] == '`' || scrap.data[i] == '"')
        escaped_size += 2;
      else
        escaped_size++;
    }
    escaped_region = malloc(sizeof(char_t *)*escaped_size+1);
    /* Escape all $ with \$, ` with \`, and " with \". This prevents
       the echo command from trying to do a variable substitution,
       command execution, and removal of double quotes.
    */
    for(int i = 0, k = 0; scrap.data[i] != '\0'; i++, k++) {
      if(scrap.data[i] == '$' || scrap.data[i] == '`' || scrap.data[i] == '"') {
        escaped_region[k] = '\\';
        k++;
        escaped_region[k] = scrap.data[i];
      } else {
        escaped_region[k] = scrap.data[i];
      }
    }
    escaped_region[escaped_size] = '\0';
    asprintf(&command, "echo \"%s\" | %s > /tmp/ait-temp.txt", (char *)escaped_region, cmd);
  } else {
    asprintf(&command, "%s%s", cmd, ots ? "" : " > /tmp/ait-temp.txt");
  }

  /* Using system(3) fixes some issues with programs such as xclip(1).
     With the ! support to control output, it also fixes issues with
     interactive shell scripts.
     It also makes the code a lot simpler.
  */
  system(command);
  if (stat("/tmp/ait-temp.txt", &sb) < 0) {
    msg("Failed to find temp file.");
    return;
  }
  if (MAX_SIZE_T < sb.st_size) {
    msg("Temp file is too big to load.");
    return;
  }
  if ((file = fopen("/tmp/ait-temp.txt", "r")) == NULL) {
    msg("Failed to open temp file.");
    return;
  }
  ngtemp = sb.st_size * sizeof (char_t);
  if(gtemp != NULL) {
    free(gtemp);
    gtemp = NULL;
  }
  if(ngtemp > 0) {
    gtemp = calloc(ngtemp + 1, sizeof(char));
    fread(gtemp, sizeof(char), ngtemp, file);
    gtemp[ngtemp-1] = '\0';  // there is usually a trailing newline
  }
  fclose(file);
  remove("/tmp/ait-temp.txt");

  for(int i = SHELLRING_SIZE-1; i > 0; i--) {
    if(shell_ring[i].data != NULL) {
      free(shell_ring[i].data);
      shell_ring[i].data = NULL;
    }
    if(shell_ring[i-1].data != NULL) {
      char_t *kri;
      kri = (char_t *)strndup((const char *)shell_ring[i-1].data, shell_ring[i-1].len);
      kri[shell_ring[i-1].len] = '\0';
      if (kri == NULL) {
       msg("No more memory available.");
       return;
      } else {
        shell_ring[i].data = kri;
        shell_ring[i].len = shell_ring[i-1].len;
      }
    }
  }
  if(shell_ring[0].data != NULL) {
    free(shell_ring[0].data);
    shell_ring[0].data = NULL;
  }
  kr = (char_t *)strndup((const char*)cmdtext, cpos);
  kr[cpos] = '\0';
  if (kr == NULL) {
    msg("No more memory available.");
   return;
  } else {
    shell_ring[0].data = kr;
    shell_ring[0].len = cpos;
  }

  memset(cmd, 0, STRBUF_L);
  memset(cmdtext, 0, STRBUF_L);

  tb_init();
  LINES = tb_height();
  COLS = tb_width();
  MSGLINE = LINES-1;
  tb_set_input_mode(TB_INPUT_ALT);

  /* Mark the log for update */
  redraw();
  /* check if canceled command */
  if(gtemp == NULL || gtemp[0] == -1 || gtemp[0] == 0) {
    if(io == 0) {
      /* put the original contents back in the buffer and reset scrap */
      paste_internal(FALSE);
      free(scrap.data);
      scrap.len = onscrap;
      scrap.data = (char_t*) malloc(scrap.len);
      (void) memcpy(scrap.data, oscrap, scrap.len * sizeof (char_t));
    }
  } else {
    switch(io) {
      case 0: {
        if(ib) {
          buffer_t *bp;
          bp = find_buffer("*Shell Command Ouput*", TRUE, FALSE);
          disassociate_b(curwp);
          curbp = bp;
          associate_b2w(curbp, curwp);
          if (!growgap(curbp, CHUNK))
            fatal("%s: Failed to allocate required memory.\n");
          movegap(curbp, 0);
          /* load the file if not already loaded */
          if (bp != NULL) {
            if (!load_file(temp)) {
              msg("");
            }
          }
        }
        clipboard();
        if(oscrap != NULL) {
          free(oscrap);
          oscrap = NULL;
        }
        break;
      }
      case 1: {
         gtemp[ngtemp-1] = '\0';
        /* Find the file name and find the line number */
        if(gtemp[0] == '\0')
          goto do_finish;
        cbuf = strtok(gtemp, ":");
        /* sometimes the editor_dir is lost */
        if(editor_dir[0] == '\0') {
          getcwd(editor_dir, PATH_MAX+1);
          strcat(editor_dir, "/");
        }
        strcpy(temp, editor_dir);
        strcat(temp, cbuf);
        cbuf = strtok(NULL, ":");
        if(cbuf != NULL && (line = atoi(cbuf)) == 0) {
          strcat(temp, ":");
          strcat(temp, cbuf);
        }
        strcat(temp, "\0");
        if(line < 1)
          free(cbuf);
        bp = find_buffer(temp, TRUE, FALSE);
        disassociate_b(curwp);
        curbp = bp;
        associate_b2w(curbp, curwp);
        if (!growgap(curbp, CHUNK))
          fatal("%s: Failed to allocate required memory.\n");
        movegap(curbp, 0);
        /* load the file if not already loaded */
        if (bp != NULL) {
           if (!load_file(temp)) {
             msg("New file %s", temp);
           }
          if(line > 0) {
            point = line_to_point(line);
            if (point != -1) {
              curbp->b_point = point;
              if (curbp->b_epage < pos(curbp, curbp->b_ebuf))
                curbp->b_reframe = 1;
              msg("Line %d", line);
            } else {
              msg("Line %d, not found", line);
            }
            update_display();
          }
        }
        break;
      }
    }
  }
do_finish:
  /* Some commands, such as ones that copy from region, will mess up
     curbp->b_line due to the cut that is preformed. We want to reset
     that mistake.
  */
  if(curbp->b_line != oline && newlines == 0 && io == 0)
    curbp->b_line = oline;

  if(command != NULL) {
    free(command);
    command = NULL;
  }
   if(gtemp != NULL) {
    free(gtemp);
    gtemp = NULL;
    ngtemp = 0;
  }
}
